/* ************************************
 * GazeVis
 * 
 * ************************************
 */

static final int KEY_FRAME_EVERY		= 10 * 1000;		// in m. sec
static final int FIX_PX_OFFSET		= 15;			// pixels 
static final int FIX_PX_OFFSET_SQ       = FIX_PX_OFFSET*FIX_PX_OFFSET;

static final int FIX_MS_OFFSET		= 100;			//in m. sec

class Fixation
{
	int timestamp;		// in m. sec
	int x, y;		// screen coordinate
	int timeElapsed;
	boolean valid;
	
	Fixation( int timestamp, int x, int y, boolean _valid )
	{
		this.timestamp = timestamp;
		this.x = x;
		this.y = y;
		this.timeElapsed = 0;
		valid = _valid;
	}
	
    boolean grThanDropTime( )
    {
        if ( this.timeElapsed > FIX_MS_OFFSET ) return true;
        else return false;
    }
    
	boolean within( Fixation prev )
	{
        if ( prev == null ) return false;
        
        int d = (x-prev.x)*(x-prev.x) + (y-prev.y)*(y-prev.y);

        //same fixation - within threshold
		if ( d <= FIX_PX_OFFSET_SQ )
        {
            //prev.avgIn( this );
            return true;
        }
        //diff fixation - not within threshold
		else return false;
	}  

    void avgIn( Fixation target )
	{
        this.x = (this.x + target.x) / 2;
        this.y = (this.y + target.y) / 2;
    }
	
}

//////////////////////////////////////////////////////////////////////////////////////////////
//
//
//////////////////////////////////////////////////////////////////////////////////////////////
class GazeLog
{
	String logFile;
	ArrayList<Fixation> fixations;
	ArrayList<Fixation> summary;
	
	HashMap<Integer, Integer> keyFrames;

	// timestamp of last fixation
	int lastTimestamp;
	
	GazeLog(String logfile)
	{
		this.logFile = logfile;
		lastTimestamp = -1;
		
		fixations = new ArrayList< Fixation >();
		keyFrames = new HashMap< Integer, Integer >();
        
        summary= new ArrayList< Fixation >();
	}
	
    /////////////////////////////////////////////////////////////////
	boolean loadGazeData()
	{
		// open file
		println("\tReading gaze file " + logFile);
		CachedFileReader input = new CachedFileReader( logFile, null );
		if (!input.isReady()) 
		{
			return false;
		}
		
		// consume the first 7 lines
		for (int i = 0; i < 7; i++)
		{
			input.readLine();
		}
		
		// load the log file
		String aLine = null;
		int firstTimestamp = -1;
		int lastKeyFrame = 0;
		
		// insert key frame 0
		keyFrames.put(0, 0);
		
		while ( null != (aLine = input.readLine()) )
		{
			ArrayList<String> tokens = tokenize( aLine );
			if (tokens.size() == 0)
			{
				continue;
			} else if (tokens.size() != 13) { continue; }
			
			// create fixation
			Fixation fix = makeFixation( tokens );
			fixations.add( fix );
			
			if (firstTimestamp == -1) 
			{
				firstTimestamp = fix.timestamp;
				fix.timestamp = 0;
			}
			else
			{
				String oldT = formatMillis( fix.timestamp );
				fix.timestamp -= firstTimestamp;
				
				// add key frame?
				int index = fix.timestamp / KEY_FRAME_EVERY;
				if (index > lastKeyFrame)
				{
					keyFrames.put( index, fixations.size() - 1 );
					lastKeyFrame = index;
				}
			}
		}
		input.closeFile();
		println("\tNumber of gazes loaded: " + fixations.size());
		
		// store last fixation
		lastTimestamp = fixations.get( fixations.size()-1 ).timestamp;
		
		return true;
	}
        
    /////////////////////////////////////////////////////////////////	
    	void summarize(  )
	{
        
	Fixation prevFix = null;
        int prevCounter = 1;
        int sumX = 0;
        int sumY = 0;

        for( int i_curFix = 0 ; i_curFix < this.fixations.size() ; i_curFix++ )
        {
            Fixation curFix = this.fixations.get(i_curFix);
            
            if( curFix.x <= 0 || curFix.y <= 0 )
            {
                //Dead gaze do nothing.
            }
            else if( prevFix == null )
            {
                prevFix = new Fixation( curFix.timestamp , curFix.x , curFix.y , true );   
            }
            else if ( curFix.within( prevFix ) ) //same fix : aggregate.
            {
                sumX += curFix.x;
                sumY += curFix.y;
                prevCounter++; 
            }
            else                            //diff fix add prev.
            {
                if( prevFix != null)
                {
                    prevFix.timeElapsed = curFix.timestamp - prevFix.timestamp;
                    prevFix.x = round( (float)sumX / (float)prevCounter );
                    prevFix.y = round( (float)sumY / (float)prevCounter );
                    
                    //if elapsed time is greater than 100ms
                    if( prevFix.grThanDropTime())   summary.add( prevFix ); 
                }                              
                
                prevFix = new Fixation( curFix.timestamp , curFix.x , curFix.y , true );                 
                sumX = prevFix.x;
                sumY = prevFix.y;
                prevCounter = 1;
            }
        }
        
        fixations = summary;
        keyFrames = new HashMap<Integer, Integer>();

	}
    
    /////////////////////////////////////////////////////////////////
    
	int quickSeek(int curIndex, int newT)
	{
		if (newT == lastTimestamp) {
			return fixations.size() - 1;
		}
		else if (newT == 0) {
			return 0;
		}
		
        int nearestIndex;
		Integer obj = keyFrames.get( newT / KEY_FRAME_EVERY );
        if ( obj == null )
        {
            nearestIndex = 0;
		}
        else
        {
            nearestIndex = obj.intValue();
        }
        
		if (nearestIndex > curIndex)
		{
			return this.seek(newT);
		}
		else
		{
			int curT = fixations.get(curIndex).timestamp;
			curIndex++;
			
			while ( curT <= newT )
			{
				Fixation fix = fixations.get( curIndex++ );
				curT = fix.timestamp;
			}
			return curIndex - 1;
		}
	}
	
	
	int seek(int t)
	{
		if (t == lastTimestamp) {
			return fixations.size() - 1;
		}
		else if (t == 0) {
			return 0;
		}
			
		// round to the nearest key frame
		int keyIndex = t / KEY_FRAME_EVERY;
		Integer obj = keyFrames.get( keyIndex );
		int i = (obj == null ? 0 : obj.intValue());
        
		int curT = fixations.get( i ).timestamp;
		int curIndex = i + 1;
		
		// seek from keyframe until we get a timestamp higher than requested
		while ( curT <= t )
		{
			Fixation fix = fixations.get( curIndex++ );
			curT = fix.timestamp;
		}
		
		return curIndex - 1;
	}
	
	Fixation getFixation( int index )
	{
		return fixations.get( index );
	}
	
	int getFixationCount()
	{
		return fixations.size();
	}
	
	String[] getFixationsData( int bT, int eT )
	{
		// get indices first
		int beginT = seek(bT);
		int endT = seek(eT);
		
		String[] data = new String[ endT - beginT + 1 + 1 ];
		data[0] = "X\t\tY\t\tTime\t\tEye found (t/f)";
		
		int curIndex = 1;
		int firstT = fixations.get(beginT).timestamp;
		
		for (int i = beginT; i <= endT; i++, curIndex++)
		{
			Fixation fix = fixations.get(i);

			data[curIndex] = "" + 
				fix.x + "\t\t" + 
				fix.y + "\t\t" +
				formatMillis( fix.timestamp - firstT ) + "\t\t" +
				(fix.valid ? "1" : "0");
		}
		
		return data;
	}

	
	int getLastTimestamp()
	{
		return lastTimestamp;
	}
	
}

//////////////////////////////////////////////////////////////////////////////////////////////
//
//
//////////////////////////////////////////////////////////////////////////////////////////////
class SubjectLog
{
	GazeLog gazeData;
	GazeVis gazeVis;
	VisVis vis;
	String directory;
	
	int oldStimState;
	
	int lowLimit, highLimit;
	
	SubjectLog( String stimuli_file, String directory )
	{
		this.directory = directory;
		String gazeLogFile = dataPath( directory + "/" + "eye.txt" );
		String visLogFile = dataPath( directory + "/" + "log.txt" );
		
		// gaze slider synch limits
		lowLimit = highLimit = -1;
		
		// load eye gaze data
		gazeData = new GazeLog( gazeLogFile );
		gazeData.loadGazeData();  
		
		if (summarize_data)
		{
			gazeData.summarize( );
		}
		
		// load vis data
		vis = new VisVis( stimuli_file, visLogFile );
		
		// create gaze visualization object
		gazeVis = new GazeVis( gazeData );

		oldStimState = 0;
	}
	
	void loadSettings()
	{
		// see if we have a settings file
		String settingsFile = dataPath( directory + "/" + "settings.txt");
		if (new File( settingsFile ).exists())
		{
			println("\tReading settings from: " + settingsFile);
			// load the file
			String[] settings = loadStrings( "data/" + directory + "/" + "settings.txt");
			parseSettings( settings );
		}
		
		if (lowLimit != -1 && highLimit != -1) {
			gazeTimeline.setLimit( lowLimit, highLimit );
			gazeTimeline.setCurrentT( lowLimit );
			this.updateGazeT( gazeTimeline.getCurrentT() );
			slidersSynched = true;
			
		}

	}
	
	
	void sync()
	{
		// sync sliders
		int visLength = visTimeline.getRangeLen();
		lowLimit = gazeTimeline.getCurrentT();
		highLimit = lowLimit + visLength;
		
		gazeTimeline.setLimit(
			lowLimit,
			highLimit
		);
	
		slidersSynched = true;
		
		this.saveSettings();
	}

	void unsync()
	{
		gazeTimeline.setLimit(-1, -1);
		slidersSynched = false;
	}

	void parseSettings( String[] settings )
	{
		for (int i = 0; i < settings.length; i++)
		{
			String[] tokens = split( settings[i], "=" );
			if (tokens.length != 2) 
			{
				println("Error parsing settings.txt. Line: " + settings[i]);
				return;
				
			}
			
			String strKey = trim(tokens[0]);
			String strValue = trim(tokens[1]);
			
			if (strKey.compareToIgnoreCase("lowLimit") == 0)
			{
				lowLimit = Integer.parseInt( strValue );
			}
			else if (strKey.compareToIgnoreCase("highLimit") == 0)
			{
				highLimit = Integer.parseInt( strValue );
			}
		}
	}
	
	void saveSettings()
	{	
		String[] settings = new String[2];
		settings[0] = "lowLimit\t\t=\t" + lowLimit;
		settings[1] = "highLimit\t\t\t=\t" + highLimit;
		
		String filename = "data/" + directory + "/" + "settings.txt";
		saveStrings( filename, settings );
	}
	
	GazeLog getGazeData()
	{
		return gazeData;
	}
	
	VisLog getVisData()
	{
		return vis.visLog;
	}
	
	void updateVisT(int t)
	{
		vis.updateT( t );
	}
	
	void updateGazeT(int t)
	{
		gazeVis.updateT( t );
	}
	
	void setGazePlayState( int state )
	{
		gazeVis.setPlayState( state );
	}
	
	void setVisPlayState( int state )
	{
		vis.setPlayState( state );
	}
	
	int getGazePlayState()
	{
		return gazeVis.state;
	}
	
	int getVisPlayState()
	{
		return vis.state;
	}
	
	void jumpToNextFixation()
	{
		gazeVis.jumpToNextFixation();
		gazeTimeline.setCurrentT( gazeVis.curT );
		
		if (slidersSynched)
		{
			visTimeline.setCurrentT( gazeTimeline.getCurrentT() - gazeTimeline.lowLimit );
			this.updateVisT( visTimeline.getCurrentT() );
		}
	}
	
	void jumpToPreviousFixation()
	{
		gazeVis.jumpToPreviousFixation();
		gazeTimeline.setCurrentT( gazeVis.curT );
		
		if (slidersSynched)
		{
			visTimeline.setCurrentT( gazeTimeline.getCurrentT() - gazeTimeline.lowLimit );
			this.updateVisT( visTimeline.getCurrentT() );
		}		
	}
	
	void render(int millisDelta)
	{
		// render the vis
		if (vis.render(millisDelta)) 
		{
			visTimeline.setState( STATE_PAUSED );
			vis.setPlayState(STATE_PAUSED);
			
			if (slidersSynched) 
			{
				gazeVis.setPlayState(STATE_PAUSED);
				gazeTimeline.setState(STATE_PAUSED);
			}
		}
		
		// render heatmap, if any
		StimulusRecord curStimulus = vis.getCurStimulus();
		if (heatmaps != null && curStimulus != null && curStimulus.phase == PHASE_VIS)
		{
			Heatmap theMap = heatmaps.get( curStimulus.number );
			image(theMap.getMap(), 0, 0);
		}
		
		int curStimState = vis.getStimState();
		if ( oldStimState != curStimState )
		{
			oldStimState = curStimState;
			gazeVis.changeState( curStimState);  
            
		}
		
		// render the gaze
		if (gazeVis.render(millisDelta)) 
		{
			gazeTimeline.setState( STATE_PAUSED );
			gazeVis.setPlayState(STATE_PAUSED);
			
			if (slidersSynched) 
			{
				visTimeline.setState( STATE_PAUSED );
				vis.setPlayState(STATE_PAUSED);
			}
				
		}
	}
	
	void playBoth()
	{
		if (globalPlay == false)
		{
			setVisPlayState(STATE_PLAYING);
			setGazePlayState(STATE_PLAYING);
			globalPlay = true;
		}
		else
		{
			setVisPlayState(STATE_PAUSED);
			setGazePlayState(STATE_PAUSED);
			globalPlay = false;
		}
	}
	
	void chopFiles()
	{
		if (!slidersSynched)
		{
			println("\t ** Vis and gaze data needs to be synched first **");
			return;	
		}
		
		vis.visLog.chopGazeData( gazeData, lowLimit );
		println("Gaze chop complete.");
	}
}

Fixation makeFixation( ArrayList<String> tokens )
{
	int x = Integer.parseInt( tokens.get(2) );
	int y = Integer.parseInt( tokens.get(3) );
	
	String strTimestamp = tokens.get( 11 );
	String[] tt = split( strTimestamp, ".");
	
	// Round to millis and exclude micro seconds
	int seconds = Integer.parseInt( tt[0] );
	int milli = Integer.parseInt( tt[1].substring(0, 3) );
	
	boolean valid = tokens.get(1).compareToIgnoreCase("1") == 0;
	int timestamp = seconds * 1000 + milli;
	
	return new Fixation(timestamp, x, y, valid);
}

ArrayList<String> tokenize( String in )
{
	ArrayList<String> tokens = new ArrayList<String>();
	String lastToken = "";
	
	for (int i = 0; i < in.length(); i++)
	{
		char c = in.charAt( i );
		if ( c == ' ' || c == '\t' ) 
		{
			if (lastToken.length() > 0)
			{
				tokens.add( trim(lastToken) );
				lastToken = "";
			}
		}
		else
		{
			lastToken += c;
		}
	}
	
	if (lastToken.length() > 0) {
		tokens.add( trim(lastToken) );
	}
	
	/*
	String strTest = "";
	for (int i = 0; i < tokens.size(); i++) {
		strTest += tokens.get(i) + ", ";
	}
	println("Tokens: " + strTest);
	*/
	
	return tokens;
}

String formatMillis( int m )
{
	return ( m / 1000 ) + "." + nf( m % 1000, 3 );
}
